1. useEffect

什么是函数的副作用

函数的副作用就是函数除了返回值外,对外界环境造成的其它影响,即与组件渲染无关的操作。例如获取数据修改全局变量更新 DOM 等。

useEffect 是 React 中的 hooks API。通过 useEffect 可以执行一些副作用操作,例如:请求数据、事件监听等。它的语法格式如下:

其中,

useEffect 的执行时机

如果没有为 useEffect 指定依赖项数组,则 Effect 中的副作用函数,会在函数组件每次渲染完成后执行。例如,我们在下面的代码中,基于 useEffect 获取 h1 元素最新的 innerHTML:

deps 为空数组

如果为 useEffect 指定了一个空数组 [] 作为 deps 依赖项,则副作用函数只会在组件首次渲染完成后执行唯一的一次。当组件 rerender 的时候不会触发副作用函数的重新执行。例如下面的代码中,useEffect 中的 console.log() 只会执行1次:

deps 为依赖项数组

如果想有条件地触发副作用函数的重新执行,则需要通过 deps 数组指定依赖项列表

React 会在组件每次渲染完成后,对比渲染前后的每一个依赖项是否发生了变化,只要任何一个依赖项发生了变化,都会触发副作用函数的重新执行。否则,如果所有依赖项在渲染前后都没有发生变化,则不会触发副作用函数的重新执行。

在一个useEffect里面,deps数组里面建议只写一个依赖项,因为如果写多个的话,可能造成不必要的重新渲染。

一个组件里面可以写多个useEffect,来达到监听不同状态的目的。

下面的例子演示了依赖项的使用:只有当 count 值发生变化时,才会触发 useEffect 回调函数的重新执行,flag 值的变化不会触发:

注意:不建议对象作为 useEffect 的依赖项,因为 React 使用 Object.is() 来判断依赖项是否发生变化。

使用props传递过来的数据可以使用useEffect进行监听。

例如:

image-20240222094119702

 

那如何监听对象、数组呢?

暂时还不知道,不确定是不是useReducer,等后面知道答案了再来回答吧。

如何清理副作用

useEffect 可以返回一个函数,用于清除副作用的回调。语法格式如下:

这里有一点刚开始不太明白,为什么要清理副作用呢?

因为副作用一般都是定时器函数或者事件监听程序,如果不清理,即使页面切换了,这些函数或程序还是会一直执行下去,产生意想不到的影响。

什么叫“清理副作用”?怎么清理?

其实就是让副作用不再起作用,副作用是自己定义的,要么是定时器、要么是事件监听函数,该怎么清除自己也应该知道。这个函数是怎么执行的呢?是react接收到了清理副作用的函数,react会调用这个函数的。

清理函数触发的时机:

实际应用场景:如果当前组件中使用了定时器或绑定了事件监听程序,可以在返回的函数中清除定时器或解绑监听程序。

组件卸载时终止未完成的 Ajax 请求

在父组件 TestRandomColor 中,使用布尔值 flag 控制子组件 RandomColor 的展示与隐藏,隐藏组件就表示卸载组件:

在子组件 RandomColor 中,通过 useEffect(fn, []) 声明一个副作用函数,该副作用函数仅在组件首次渲染完毕后执行。在该副作用函数中,基于 fetch API 请求数据,并且在清理函数中使用 AbortController 对象自动终止未完成的 Ajax 请求。

AbortController 对象是一个JS API,可以用来取消 Fetch 请求,它的 signal 属性指向一个 AbortSignal 对象。AbortSignal 对象可以用来检测 Fetch 请求是否已经被取消,从而停止处理响应数据。

示例代码如下:

正常请求:

在请求开始时,卸载子组件,查看请求是否停止:

可以看到请求终止了。

获取鼠标在网页中移动时的位置

示例代码如下,先声明一个 MouseInfo 的子组件,用来监听鼠标的移动并打印鼠标的位置:

再声明一个 TestMouseInfo 的父组件,通过布尔值 flag 控制子组件 MouseInfo 的显示或隐藏:

子组件卸载后,移动鼠标,没有输出,说明绑定的事件已经被清除了,也就是执行了清除副作用。

优化

鼠标的监听没有节流,如果在一个大项目里面,会造成更新太频繁,会很卡顿,所以加上节流是必须的。

可以看到,在1000ms之内,鼠标移动了很多的位置,但是只有在1000ms之后的位置,才会更新位置信息。

注意:

如果想自定义延迟时间,需要在父组件中传递参数,在子组件中使用props来接受参数。

自定义封装鼠标位置的 hook

不要被自定义hook难住了,更不要以为自定义hook一定要按照useState或其它官方hooks的形式来做,useState的返回值一个是数值,一个是函数,我也非这样做不可,千万不要这么想。

自定义hook就是一个功能函数,完成所需的功能即可,形式可以多种多样。

注意:

不要一开始就想写成一个hook,这还是很难的,最好还是先在子组件里面实现功能,然后将功能代码剪切到定义的hook里面,然后调用hook查看是否能正常使用。

这样写代码才会简单一些。

src 目录下新建 hooks/index.ts 模块,并把刚才获取鼠标位置的代码封装成名为 useMousePosition 的自定义 hook,代码如下:

MouseInfo 组件中,可以导入自己封装的 hook 进行使用:

在 TestMouseInfo 组件中,也可以导入自己封装的 hook 进行使用:

添加节流的useMouseInfo:

学习到这里,其实我有一个疑问?为什么自定义的hooks能够触发组件的重新渲染呢?

因为自定义的hooks里面用到了react官方的hooks,如果没有用到react官方的hooks,那就相当于是一个普通组件或者函数了,就不能称之为“自定义hooks”了。

上面的自定义hooks有一个ts的报错:

image-20240223141813881

image-20240223141829997

看错误提示信息,应该是没有在useEffect的依赖数组里面添加delay这个依赖,但肯定不能加上,也不能去掉依赖项数组。所以最好的方法还是先忽略掉这个ts报错,搜索了一下:https://juejin.cn/post/7133968417404485663

image-20240223142417318

只能先忽略掉这个ts报错,因为别的方法都特别复杂。

自定义封装秒数倒计时的 hook

功能分析:

  1. 用户调用 useCountDown(5) 的 hook,可以传递倒计时的秒数,如果未指定秒数则默认值为 10 秒
  2. useCountDown 中,需要对用户传递进行来的数字进行非法值的判断和处理(处理负数、小数、0)
  3. 每隔1秒让秒数 -1,并使用一个布尔值记录按钮是否被禁用
  4. 以数组的形式,向外返回每次的秒数和当前的禁用状态,例如 return [count, disabled]

最终,用户可以按照如下的方式,使用我们封装的 useCountDown hook:

上面这种写法很牛逼啊,直接在button标签上就写好了。如果要我使用vue或小程序来写,估计会使用很多变量来控制。

接下来,我们可以在 src/hooks/index.ts 模块中,封装名为 useCountDown 的自定义 hook。具体代码如下:

注意:

这个倒计时的写法思路要搞清楚,为什么是用setTimeout(),而不是我一想就想到的setInterval()?为什么在else里面要清除定时器,在useEffect里面也要返回清除定时器的函数?

可以使用setInterval(),useEffect()的依赖项数组设置为空数组就行了,但是这个interval的终止条件怎么写呢?可以写在serInterval()里面,试一下:

一定要检查一下这样的效果怎么样,记录下来。

可以看到在count===0时,计时器并没有停下来,为什么呢?输出看一下:

可以看到interval里面的count值一直没有变化,就是初始值,为什么呢?interval里面的函数不是每1秒钟执行一次吗?执行的时候拿到的不应该是最新值吗?

搜索了一下,setInterval是每隔一段时间,调用一次fn,这个fn是不变的,所以里面的变量也是不变的。那为什么setCount又能改变值呢?因为setCount是异步执行,拿到了最新值。

 

来看第二个问题,else里面的清除只会执行一次,而useEffect()里面的清除函数,是在每次useEffect()执行之前都会执行,这样timerId才不会重复,setTimeout才会执行下去。

------2024.02.23

上面这段话有错误,setTimeout的timerId不涉及到重复问题,如果不清除也可以正常执行,不会报错。我写成这样也是正常执行没有报错的:

image-20240223144741527

 

为了避免内存泄漏和不必要的资源消耗,需要在适当的时机清除setTimeout计时器,所以老师的代码中写了。

useEffect 的使用注意事项

  1. 不要在 useEffect 中改变依赖项的值,会造成死循环。

例子:

注意:

这里的意思很明确了,不要在useEffect中改变依赖项的值。也就是说useEffect的deps是一个数组,并且数组里面有依赖项。前面的例子中,deps都是空数组,意味着只在组件初次渲染的时候执行一次,所以是没有问题的。

千万不要理解错了意思。

同时还要注意一点:在useEffect中改变依赖项的值,如果不写依赖项数组,也会造成死循环。因为不写依赖项数组,则每次组件更新都会执行一次,而执行一次就会改变一次依赖项的值,这样就是死循环。

 

  1. 多个不同功能的副作用尽量分开声明,不要写到一个 useEffect 中。

例子:

本意应该是单独监听count和flag的改变,然后执行count或flag相应的代码。但是改变count或flag之后,都输出了。所以应该分开写useEffect。

分开写useEffect之后,在组件初次渲染的时候,都会先执行一次,之后就是监听各自的依赖项来执行副作用函数了。

2. useLayoutEffect

useLayoutEffect 和 useEffect 的对比

1. 用法相似

useLayoutEffect 和 useEffect 的使用方式很相似:

  1. useLayoutEffect 接收一个函数和一个依赖项数组作为参数
  2. 只有在数组中的依赖项发生改变时才会再次执行副作用函数
  3. useLayoutEffect 也可以返回一个清理函数

2. 区别

hooks 名称执行时机执行过程
useEffect在浏览器重新绘制屏幕之后触发异步执行,不阻塞浏览器绘制
useLayoutEffect在浏览器重新绘制屏幕之前触发同步执行,阻塞浏览器重新绘制

注意:React 保证了 useLayoutEffect 中的代码以及其中任何计划的状态更新都会在浏览器重新绘制屏幕之前得到处理。

这里的“在浏览器重新绘制屏幕之前触发”要理解清楚,此时拿到的useState定义的值是最新的,只是执行时机是在浏览器重新绘制之前触发。

3. 代码示例

点击按钮,把 num 值设置为 0,当页面更新完成后,判断 num 是否等于 0,如果等于 0,则在 useEffect 中把 num 赋值为随机的数字:

运行上面的代码,我们会发现这串数字会出现闪烁的情况。原因是页面会先将 h1 渲染为 0,然后再渲染成随机的数字,由于更新的很快便出现了闪烁。下面的动图可能看的不是很清楚,原因是gif录制时的帧数不够,实际操作一遍,就很明显的看到抖动。

为了解决上述问题,可以把 useEffect 替换为 useLayoutEffect:

更改完成后再次运行代码,发现数字不再闪烁了。因为点击按钮时,num 更新为 0,但此时页面不会渲染,而是等待 useLayoutEffect 内部状态修改后才会更新页面,所以不会出现闪烁。

上面的这个例子好像没有实际作用、不会遇到,但是在实际编写代码的时候,也许不自觉的就造成了这种现象,所以遇到这种现象的时候,就要想到有useLayoutEffect这个hooks,可以解决问题。